/* 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * For more ,please contact QQ/wechat:4108863 mail:4108863@qq.com
 */


#include <sys/select.h>
#include <sys/socket.h>

#include <unistd.h>

#include "logging.h"
#include "hihttps.h"
#include "ocsp.h"
#include "configuration.h"

/* hihttps.c */
extern hihttps_config *CONFIG;
extern struct ev_loop *loop;

void
HOCSP_free(sslstaple **staple)
{
    if (*staple == NULL)
        return;
    free((*staple)->staple);
    FREE_OBJ(*staple);
    *staple = NULL;
}


int
HOCSP_verify(sslctx *sc, OCSP_RESPONSE *resp, double *nextupd)
{
    OCSP_BASICRESP *br = NULL;
    X509_STORE *store;
    STACK_OF(X509) *chain = NULL;
    OCSP_CERTID *cid = NULL;
    int status = -1, reason;
    ASN1_GENERALIZEDTIME *asn_nextupd = NULL;
    X509 *issuer;
    int i;
    int do_verify = sc->staple_vfy;
    int verify_flags = OCSP_TRUSTOTHER;

    if (sc->staple_vfy < 0)
        do_verify = CONFIG->OCSP_VFY;

    if (!do_verify)
        verify_flags |= OCSP_NOVERIFY;

    store = SSL_CTX_get_cert_store(sc->ctx);
    AN(store);

#ifdef SSL_CTRL_GET_CHAIN_CERTS
    AN(SSL_CTX_get0_chain_certs(sc->ctx, &chain));
#else
    chain = sc->ctx->extra_certs;
#endif
    br = OCSP_response_get1_basic(resp);
    if (br == NULL) {
        ERR("{core} OCSP_response_get1_basic failed (cert: %s)\n",
            sc->filename);
        goto err;
    }
    i = OCSP_basic_verify(br, chain, store, verify_flags);
    if (i <= 0) {
        log_ssl_error(NULL, "{core} Staple verification failed "
            "for cert %s\n", sc->filename);
        goto err;
    }

    issuer = Find_issuer(sc->x509, chain);
    if (issuer == NULL) {
        ERR("{core} Unable to find issuer for cert %s\n.",
            sc->filename);
        goto err;
    }

    cid = OCSP_cert_to_id(NULL, sc->x509, issuer);
    if (cid == NULL) {
        ERR("{core} OCSP_cert_to_id failed\n");
        goto err;
    }

    if (OCSP_resp_find_status(br, cid, &status, &reason,
        NULL, NULL, &asn_nextupd) != 1) {
        ERR("{core} OCSP_resp_find_status failed: Unable to "
            "find OCSP response with a matching certificate id\n");
        goto err;
    }

    if (status != V_OCSP_CERTSTATUS_GOOD) {
        ERR("{core} Certificate %s has status %s\n", sc->filename,
            OCSP_cert_status_str(status));
        if (status == V_OCSP_CERTSTATUS_REVOKED)
            ERR("{core} Certificate %s revocation reason: %s\n",
                sc->filename, OCSP_crl_reason_str(reason));
        goto err;
    }

    if (asn_nextupd != NULL)
        *nextupd = asn1_gentime_parse(asn_nextupd);
    else {
        *nextupd = -1.0;
    }

    OCSP_CERTID_free(cid);
    OCSP_BASICRESP_free(br);
    return (0);

err:
    if (cid != NULL)
        OCSP_CERTID_free(cid);
    if (br != NULL)
        OCSP_BASICRESP_free(br);
    return (1);
}

#ifndef OPENSSL_NO_TLSEXT
int
HOCSP_staple_cb(SSL *ssl, void *priv)
{
    sslstaple *staple;
    unsigned char *buf;
    CAST_OBJ_NOTNULL(staple, priv, SSLSTAPLE_MAGIC);

    if (staple->nextupd != -1 &&
        staple->nextupd < Time_now()) {
        return (SSL_TLSEXT_ERR_NOACK);
    }

    /* SSL_set_tlsext_status_ocsp_resp will issue a free() on the
     * provided input, so we need to pass a copy. */
    buf = malloc(staple->len);
    AN(buf);
    memcpy(buf, staple->staple, staple->len);

    if (SSL_set_tlsext_status_ocsp_resp(ssl,
        buf, staple->len) == 1)
        return (SSL_TLSEXT_ERR_OK);
    else
        free(buf);

    return (SSL_TLSEXT_ERR_NOACK);
}
#endif


int
HOCSP_init_resp(sslctx *sc, OCSP_RESPONSE *resp)
{
    sslstaple *staple = NULL;
    int len, i;
    unsigned char *tmp, *buf;

    i = OCSP_response_status(resp);
    if (i != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
        ERR("{core} Error: OCSP response for cert %s has status %s\n",
            sc->filename, OCSP_response_status_str(i));
        goto err;
    }

    len = i2d_OCSP_RESPONSE(resp, NULL);
    if (len < 0) {
        log_ssl_error(NULL, "{core} i2d_OCSP_RESPONSE");
        goto err;
    }
    buf = malloc(len);
    AN(buf);
    tmp = buf;
    i = i2d_OCSP_RESPONSE(resp, &tmp);
    assert(i > 0);

    ALLOC_OBJ(staple, SSLSTAPLE_MAGIC);
    AN(staple);
    staple->staple = buf;
    staple->len = len;

    if (HOCSP_verify(sc, resp, &staple->nextupd) != 0) {
        goto err;
    }

    if (!SSL_CTX_set_tlsext_status_cb(sc->ctx, HOCSP_staple_cb)) {
        ERR("Error configuring status callback.\n");
        goto err;
    } else if (!SSL_CTX_set_tlsext_status_arg(sc->ctx, staple)) {
        ERR("Error setting status callback argument.\n");
        goto err;
    }

    if (sc->staple != NULL)
        HOCSP_free(&sc->staple);
    sc->staple = staple;
    return (0);

err:
    if (staple != NULL)
        HOCSP_free(&staple);
    return (1);
}


static void
hocsp_stat_cb(struct ev_loop *loop, ev_stat *w, int revents)
{
    sslctx *sc;
    sslstaple *oldstaple;

    (void) revents;
    (void) loop;
    CAST_OBJ_NOTNULL(sc, w->data, SSLCTX_MAGIC);

    if (w->attr.st_nlink) {
        oldstaple = sc->staple;
        sc->staple = NULL;
        AN(sc->staple_fn);

        if (HOCSP_init_file(sc->staple_fn, sc, 1) != 0) {
            sc->staple = oldstaple;
            return;
        }

        HOCSP_free(&oldstaple);
        LOG("{core} Loaded cached OCSP staple for cert '%s'\n",
            sc->filename);
    }
}

void
HOCSP_ev_stat(sslctx *sc)
{
    char *fn;
    STACK_OF(OPENSSL_STRING) *sk_uri = NULL;
    AN(sc->x509);
    sk_uri = X509_get1_ocsp(sc->x509);

    if (sk_uri == NULL
       || sk_OPENSSL_STRING_num(sk_uri) == 0) {
        goto err;
    }

    fn = HOCSP_fn(sc->filename);
    if (fn == NULL)
        goto err;

    free(sc->staple_fn);
    sc->staple_fn = fn;
    sc->ev_staple = malloc(sizeof *sc->ev_staple);
    AN(sc->ev_staple);
    sc->ev_staple->data = sc;
    ev_stat_init(sc->ev_staple, hocsp_stat_cb, fn, 0);

err:
    if (sk_uri != NULL)
        X509_email_free(sk_uri);
}

static OCSP_REQUEST *
hocsp_mkreq(ocspquery *oq)
{
    OCSP_REQUEST *req;
    OCSP_CERTID *cid;
    STACK_OF(X509) *chain = NULL;
    X509 *issuer;

    CHECK_OBJ_NOTNULL(oq, OCSPQUERY_MAGIC);
    CHECK_OBJ_NOTNULL(oq->sctx, SSLCTX_MAGIC);

#ifdef SSL_CTRL_GET_CHAIN_CERTS
    AN(SSL_CTX_get0_chain_certs(oq->sctx->ctx, &chain));
#else
    chain = oq->sctx->ctx->extra_certs;
#endif
    issuer = Find_issuer(oq->sctx->x509, chain);
    if (issuer == NULL) {
        ERR("{ocsp} Unable to find issuer for cert %s\n.",
            oq->sctx->filename);
        return (NULL);
    }

    cid = OCSP_cert_to_id(NULL, oq->sctx->x509, issuer);
    if (cid == NULL) {
        ERR("{ocsp} OCSP_cert_to_id failed for cert %s\n",
            oq->sctx->filename);
        return (NULL);
    }

    req = OCSP_REQUEST_new();
    if (req == NULL) {
        ERR("{ocsp} OCSP_REQUEST_new failed\n");
        OCSP_CERTID_free(cid);
        return (NULL);
    }

    if (OCSP_request_add0_id(req, cid) == NULL) {
        ERR("{ocsp} OCSP_request_add0_id failed\n");
        OCSP_CERTID_free(cid);
        OCSP_REQUEST_free(req);
        return (NULL);
    }

    return (req);
}


/* Save a downloaded staple to the file system.
 * Process: OCSP child  */
static int
hocsp_proc_persist(sslctx *sc)
{
    char *dstfile = NULL;
    int fd = -1;
    struct vsb *tmpfn;

    CHECK_OBJ_NOTNULL(sc, SSLCTX_MAGIC);
    CHECK_OBJ_NOTNULL(sc->staple, SSLSTAPLE_MAGIC);
    dstfile = HOCSP_fn(sc->filename);
    if (dstfile == NULL)
        return (1);

    (void)umask(027);

    tmpfn = VSB_new_auto();
    AN(tmpfn);
    VSB_printf(tmpfn, "%s.XXXXXX", dstfile);
    VSB_finish(tmpfn);
    fd = mkstemp(VSB_data(tmpfn));
    if (fd < 0) {
        if (errno == EACCES)
            ERR("{ocsp} Error: ocsp-dir '%s' is not "
                "writable for the configured user\n",
                CONFIG->OCSP_DIR);
        else
            ERR("{ocsp} hocsp_proc_persist: mkstemp: %s: %s\n",
                VSB_data(tmpfn), strerror(errno));
        goto err;
    }

    if (write(fd, sc->staple->staple, sc->staple->len) != sc->staple->len) {
        ERR("{ocsp} hocsp_proc_persist: write: %s\n", strerror(errno));
        (void) close(fd);
        goto err;
    }

    if(close(fd) != 0) {
        ERR("{ocsp} hocsp_proc_persist: close: %s\n", strerror(errno));
        goto err;
    }

    if (rename(VSB_data(tmpfn), dstfile) != 0) {
        ERR("{ocsp} hocsp_proc_persist: rename: %s: %s\n",
            strerror(errno), dstfile);
        goto err;
    }

    /* worker procs notified via ev_stat (inotify/stat) */

    VSB_delete(tmpfn);
    free(dstfile);
    return (0);

err:
    unlink(VSB_data(tmpfn));
    VSB_delete(tmpfn);
    free(dstfile);
    return (1);
}


int
HOCSP_init_file(const char *ocspfn, sslctx *sc, int is_cached)
{
    BIO *bio;
    OCSP_RESPONSE *resp;

    if (ocspfn == NULL) {
        return (1);
    }

    bio = BIO_new_file(ocspfn, "r");
    if (bio == NULL) {
        if (is_cached)
            return (1);
        ERR("Error loading status file '%s'\n", ocspfn);
        return (1);
    }

    resp = d2i_OCSP_RESPONSE_bio(bio, NULL);
    BIO_free(bio);
    if (resp == NULL) {
        ERR("Error parsing OCSP staple in '%s'\n", ocspfn);
        return (1);
    }

    if (HOCSP_init_resp(sc, resp) != 0)
        goto err;

    CHECK_OBJ_NOTNULL(sc->staple, SSLSTAPLE_MAGIC);
    OCSP_RESPONSE_free(resp);
    return (0);

err:
    if (resp != NULL)
        OCSP_RESPONSE_free(resp);
    return (1);
}


char *
HOCSP_fn(const char *certfn)
{
    EVP_MD_CTX *mdctx = NULL;
    unsigned char md_val[EVP_MAX_MD_SIZE];
    unsigned int i, md_len;
    struct vsb *vsb;
    char *res;

    if (CONFIG->OCSP_DIR == NULL) {
        ERR("{ocsp} Error: OCSP directory not specified.\n");
        return (NULL);
    }

    mdctx = EVP_MD_CTX_create();
    if (mdctx == NULL) {
        ERR("{ocsp} EVP_MD_CTX_create failed\n");
        goto err;
    }
    if (EVP_DigestInit_ex(mdctx, EVP_md5(), NULL) != 1) {
        ERR("{ocsp} EVP_DigestInit_ex in ocsp_fn() failed\n");
        goto err;
    }
    if (EVP_DigestUpdate(mdctx, certfn, strlen(certfn)) != 1) {
        ERR("{ocsp} EVP_DigestUpdate in ocsp_fn() failed\n");
        goto err;
    }
    if (EVP_DigestFinal_ex(mdctx, md_val, &md_len) != 1) {
        ERR("{ocsp} EVP_DigestFinal_ex in ocsp_fn() failed\n");
        goto err;
    }

    EVP_MD_CTX_destroy(mdctx);

    vsb = VSB_new_auto();
    AN(vsb);
    VSB_cat(vsb, CONFIG->OCSP_DIR);
    VSB_putc(vsb, '/');
    for (i = 0; i < md_len; i++)
        VSB_printf(vsb, "%02x", md_val[i]);
    VSB_finish(vsb);
    res = strdup(VSB_data(vsb));
    AN(res);
    VSB_delete(vsb);
    return (res);

err:
    if (mdctx != NULL)
        EVP_MD_CTX_destroy(mdctx);
    return (NULL);
}

static void hocsp_query_responder(struct ev_loop *loop, ev_timer *w, int revents);


/* Start a per-sslctx evloop timer that downloads the OCSP staple.
 * Process: OCSP child  */
void
HOCSP_mktask(sslctx *sc, ocspquery *oq, double refresh_hint)
{
    double refresh = -1.0;
    double tnow;
    STACK_OF(OPENSSL_STRING) *sk_uri;

    tnow = Time_now();

    if (sc->staple != NULL) {
        CHECK_OBJ_NOTNULL(sc->staple, SSLSTAPLE_MAGIC);
        if (sc->staple->nextupd > 0) {
            refresh = sc->staple->nextupd - tnow - 600;
            if (refresh < 0)
                refresh = 0.0;
        } else
            refresh = CONFIG->OCSP_REFRESH_INTERVAL;
    } else {
        AN(sc->x509);
        sk_uri = X509_get1_ocsp(sc->x509);
        if (sk_uri == NULL || sk_OPENSSL_STRING_num(sk_uri) == 0) {
            LOG("{ocsp} Note: No OCSP responder URI found "
                "for cert %s\n", sc->filename);
            if (sk_uri != NULL)
                X509_email_free(sk_uri);
            return;
        }
        /* schedule for immediate retrieval */
        X509_email_free(sk_uri);
        refresh = 0.0;
    }

    if (refresh < refresh_hint)
        refresh = refresh_hint;

    if (oq == NULL)
        ALLOC_OBJ(oq, OCSPQUERY_MAGIC);

    CHECK_OBJ_NOTNULL(oq, OCSPQUERY_MAGIC);
    oq->sctx = sc;

    assert(refresh >= 0.0);
    ev_timer_init(&oq->ev_t_refresh,
        hocsp_query_responder, refresh, 0.);
    oq->ev_t_refresh.data = oq;
    ev_timer_start(loop, &oq->ev_t_refresh);

    LOG("{ocsp} Refresh of OCSP staple for %s scheduled in "
        "%.0lf seconds\n", sc->filename, refresh);
}



static void
hocsp_query_responder(struct ev_loop *loop, ev_timer *w, int revents)
{
    ocspquery *oq;
    OCSP_REQUEST *req = NULL;
    OCSP_REQ_CTX *rctx = NULL;
    STACK_OF(OPENSSL_STRING) *sk_uri;
    char *host = NULL, *port = NULL, *path = NULL;
    int https = 0;
    BIO *cbio = NULL, *sbio;
    SSL_CTX *ctx = NULL;
    OCSP_RESPONSE *resp = NULL;
    double resp_tmo;
    fd_set fds;
    struct timeval tv;
    int n, fd;
    double refresh_hint = 60;

    (void) loop;
    (void) revents;

    CAST_OBJ_NOTNULL(oq, w->data, OCSPQUERY_MAGIC);

    sk_uri = X509_get1_ocsp(oq->sctx->x509);
    AN(sk_uri);

    AN(OCSP_parse_url(sk_OPENSSL_STRING_value(sk_uri, 0),
        &host, &port, &path, &https));
    X509_email_free(sk_uri);

    req = hocsp_mkreq(oq);
    if (req == NULL) {
        /* If we weren't able to create a request, there is no
         * use in scheduling a retry. */
        FREE_OBJ(oq);
        goto err;
    }

    /* printf("host: %s port: %s path: %s ssl: %d\n", */
    /*     host, port, path, https); */

    cbio = BIO_new_connect(host);
    if (cbio == NULL) {
        refresh_hint = 60;
        goto retry;
    }

    if (port == NULL) {
        if (https)
            port = "443";
        else
            port = "80";
    }
    AN(BIO_set_conn_port(cbio, port));

    if (https) {
        ctx = SSL_CTX_new(SSLv23_client_method());
        AN(ctx);
        SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2);
        SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3);
        SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
        sbio = BIO_new_ssl(ctx, 1);
        if (sbio == NULL) {
            ERR("{ocsp} BIO_new_ssl failed: %s\n", strerror(errno));
            refresh_hint = 60;
            goto retry;
        }
        cbio = BIO_push(sbio, cbio);
        AN(cbio);
    }

    /* set non-blocking */
    BIO_set_nbio(cbio, 1);
    n = BIO_do_connect(cbio);
    if (n <= 0 && !BIO_should_retry(cbio)) {
        ERR("{ocsp} Error connecting to %s:%s\n", host, port);
        refresh_hint = 300;
        goto retry;
    }

    assert(BIO_get_fd(cbio, &fd) >= 0);

    if (n <= 0) {
        FD_ZERO(&fds);
        FD_SET(fd, &fds);
        tv.tv_sec = CONFIG->OCSP_CONN_TMO;
        tv.tv_usec = (CONFIG->OCSP_CONN_TMO - tv.tv_sec) * 1e6;
        n = select(fd + 1, NULL, (void *) &fds, NULL, &tv);
        if (n == 0) {
            /* connect timeout */
            ERR("{ocsp} Error: Connection to %s:%s timed out. "
                "Hit parameter 'ocsp-connect-tmo"
                " [current value: %.3fs]\n",
                host, port, CONFIG->OCSP_CONN_TMO);
            refresh_hint = 300;
            goto retry;
        } else if (n < 0) {
            ERR("{ocsp} Error: Connecting to %s:%s failed: "
                "select: %s\n",
                host, port, strerror(errno));
            refresh_hint = 300;
            goto retry;
        }
    }

    rctx = OCSP_sendreq_new(cbio, path, NULL, 0);
    if (rctx == NULL) {
        ERR("{ocsp} OCSP_sendreq_new failed\n");
        refresh_hint = 60;
        goto retry;
    }
    if (OCSP_REQ_CTX_add1_header(rctx, "Host", host) == 0) {
        ERR("{ocsp} OCSP_REQ_CTX_add1_header failed\n");
        refresh_hint = 60;
        goto retry;
    }
    if (OCSP_REQ_CTX_set1_req(rctx, req) == 0) {
        ERR("{ocsp} OCSP_REQ_CTX_set1_req failed\n");
        refresh_hint = 60;
        goto retry;
    }

    resp_tmo = Time_now() + CONFIG->OCSP_RESP_TMO;
    while (1) {
        double tnow;
        n = OCSP_sendreq_nbio(&resp, rctx);
        if (n == 0) {
            /* this is an error, and we can't continue */
            ERR("{ocsp} OCSP_sendreq_nbio failed for %s:%s.\n",
                host, port);
            refresh_hint = 300;
            goto retry;
        } else if (n == 1) {
            /* complete */
            break;
        }

        FD_ZERO(&fds);
        FD_SET(fd, &fds);

        tnow = Time_now();
        tv.tv_sec = resp_tmo - tnow;
        tv.tv_usec = ((resp_tmo - tnow) - tv.tv_sec) * 1e6;

        if (BIO_should_read(cbio))
            n = select(fd + 1, (void *) &fds, NULL, NULL, &tv);
        else if (BIO_should_write(cbio))
            n = select(fd + 1, NULL, (void *) &fds, NULL, &tv);
        else {
            /* BIO_should_io_special? */
            refresh_hint = 300;
            goto retry;
        }

        if (n == -1) {
            if (errno == EINTR)
                continue;
            ERR("{ocsp} Error: Transmission failed:"
                " select: %s\n", strerror(errno));
            refresh_hint = 300;
            goto retry;
        }

        if (n == 0) {
            /* timeout */
            ERR("{ocsp} Error: Transmission timeout for %s:%s. "
                "Consider increasing parameter 'ocsp-resp-tmo'"
                " [current value: %.3fs]\n",
                host, port, CONFIG->OCSP_RESP_TMO);
            refresh_hint = 300;
            goto retry;
        }
    }

    if (resp == NULL) {
        /* fetch failed.  Retry later. */
        refresh_hint = 600.0;
    } else {
        if (HOCSP_init_resp(oq->sctx, resp) == 0) {
            LOG("{ocsp} Retrieved new staple for cert %s\n",
                oq->sctx->filename);
            if (hocsp_proc_persist(oq->sctx) != 0) {
                refresh_hint = 300;
                goto retry;
            }
        } else {
            refresh_hint = 300;
            goto retry;
        }
    }

retry:
    HOCSP_mktask(oq->sctx, oq, refresh_hint);
err:
    if (rctx)
        OCSP_REQ_CTX_free(rctx);
    if (req)
        OCSP_REQUEST_free(req);
    if (resp)
        OCSP_RESPONSE_free(resp);
    if (cbio)
        BIO_free_all(cbio);
    if (ctx)
        SSL_CTX_free(ctx);
}

